package com.tjs.lucky.interfaces.facade;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.lucky.tjs.CronUtil;
import com.lucky.tjs.config.InitJobSchedulerHandler;
import com.lucky.tjs.entity.JobConfig;
import com.tjs.lucky.application.process.OpenAwardProcess;
import com.tjs.lucky.application.worker.InitActivityJob;
import com.tjs.lucky.common.ActivityState;
import com.tjs.lucky.common.Result;
import com.tjs.lucky.domain.activity.model.vo.ActivityVO;
import com.tjs.lucky.domain.activity.repository.IUserTakeActivityRepository;
import com.tjs.lucky.domain.activity.service.deploy.ActivityDeploy;
import com.tjs.lucky.domain.strategy.repository.IStrategyRepository;
import com.tjs.lucky.infrastructure.utils.OptimisticLock;
import com.tjs.lucky.rpc.IActivityBooth;
import com.tjs.lucky.application.process.ActivityProcess;
import com.tjs.lucky.application.process.req.DrawProcessReq;
import com.tjs.lucky.application.process.res.DrawProcessResult;
import com.tjs.lucky.common.ResponseCode;
import com.tjs.lucky.domain.strategy.model.vo.DrawAwardVO;
import com.tjs.lucky.rpc.entity.ActivityConfig;
import com.tjs.lucky.rpc.dto.AwardDTO;
import com.tjs.lucky.interfaces.assembler.IMapping;
import com.tjs.lucky.rpc.req.DrawReq;
import com.tjs.lucky.rpc.res.DrawRes;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @Author: 田佳澍
 * @Description: 抽奖活动展台： 普通抽奖 量化抽奖
 * @DateTime: 2023/6/7 15:31
 **/
@Slf4j
@Service
//@DubboService
public class LotteryActivityBooth implements IActivityBooth {

    @Resource
    private ActivityProcess activityProcess;

    @Resource
    @Lazy  //单例Bean 注入其他类型Bean，其他类型的Bean会失效。
    private InitActivityJob initActivityJob;

    @Resource
    private InitJobSchedulerHandler initJobSchedulerHandler;

    @Resource
    private IMapping<DrawAwardVO, AwardDTO> awardMapping;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private IStrategyRepository strategyRepository;

    @Resource
    private IUserTakeActivityRepository userTakeActivityRepository;

    @Resource
    private OpenAwardProcess openAwardProcess;

    @Resource
    private OptimisticLock optimisticLock;

    @Resource
    private ActivityDeploy activityDeploy;

    @Override
    /**
     * 好的判断顺序也会影响接口性能
     * 奖品库存可以是无限的，如果库存数量小于0或null则默认为无限
     * 活动库存必须有次数限制
     */
    public Result doDraw(DrawReq drawReq) {
        try {
            //奖品是否充足，活动库存是否充足？
            if (activityProcess.checkStock(drawReq.getActivityId())) {
                log.info("库存 or 参与次数不足，用户:{},活动:{}", drawReq.getUId(), drawReq.getActivityId());
                return new Result(ResponseCode.SUCCESS.getCode(), "库存|参与次数不足");
            }

            //是否已有抽奖结果？
            String drawAward = stringRedisTemplate.opsForValue().get("Lucky:user:" + drawReq.getUId() + "-" + drawReq.getActivityId());
            if (StringUtils.isNoneBlank(drawAward)) {
                //续期
                log.info("已有抽奖结果，用户:{},活动:{}", drawReq.getUId(), drawReq.getActivityId());
                stringRedisTemplate.expire("Lucky:user:" + drawReq.getUId(), 2, TimeUnit.HOURS);
                return new Result(ResponseCode.SUCCESS.getCode(), "抽奖已完成，不可重复抽奖");
            }

            log.info("抽奖，开始 uId：{} activityId：{}", drawReq.getUId(), drawReq.getActivityId());
            //执行抽奖
            DrawProcessResult drawProcessResult = activityProcess.doDrawProcess(
                    new DrawProcessReq(drawReq.getUId(), drawReq.getActivityId(),drawReq.getRuleId(),drawReq.getValMap())
            );
            if (!ResponseCode.SUCCESS.getCode().equals(drawProcessResult.getCode())) {
                log.error("抽奖，失败(抽奖过程异常) uId：{} activityId：{}", drawReq.getUId(), drawReq.getActivityId());
                return new Result(drawProcessResult.getCode(), drawProcessResult.getInfo());
            }
            DrawAwardVO drawAwardVO = drawProcessResult.getDrawAwardVO();
            String data = JSONUtil.toJsonStr(drawAwardVO);

            //保存抽奖结果 保证幂等性
            stringRedisTemplate.opsForValue().setIfAbsent("Lucky:user:" + drawReq.getUId() + "-" + drawReq.getActivityId(), data, 2, TimeUnit.HOURS);

            log.info("抽奖，完成 uId：{} activityId：{} drawRes：{}", drawReq.getUId(), drawReq.getActivityId(), data);

            return new Result(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo());


        } catch (Exception e) {
            log.error("抽奖，失败 uId：{} activityId：{} reqJson：{}", drawReq.getUId(), drawReq.getActivityId(), JSON.toJSONString(drawReq), e);
            return new DrawRes(ResponseCode.UN_ERROR.getCode(), ResponseCode.UN_ERROR.getInfo());
        }
    }

    /*
    Lucky:user:tianjiashu-100002:
    {
        "awardId": "102",
        "strategyMode": 1,
        "grantDate": 1689600363000,
        "awardLeftStockCount": 20,
        "activityLeftPartCount": 100,
        "awardName": "手机",
        "awardContent": "请联系活动组织者 fustack",
        "strategyId": 10002,
        "grantType": 1,
        "awardType": 1
    }
     */

    /*
        try catch的范围要衡量好，以便对于不同范围代码异常，做不同处理
     */

    @Override //保证接口的幂等性、提高接口的并发、保证数据的一致性。
    public DrawRes doOpenAward(String uId, Long activityId) {
        // 获取奖品数据
        String userAwardKey = "Lucky:user:" + uId + "-" + activityId;
        String data = stringRedisTemplate.opsForValue().get(userAwardKey);
        if (StringUtils.isBlank(data)) {
            log.info("缓存中的奖品数据为空... 用户:{} ,活动ID:{}", uId, activityId);
            return new DrawRes(ResponseCode.LOSING_DRAW);
        }
        //保证幂等性，key由用户ID 和 活动ID组成，保证锁的细粒度
        RLock lock = redissonClient.getLock("LuckyDog:doOpen:" + uId + "-" + activityId);
        if (!lock.tryLock()) {
            log.info("获取分布式锁失败... 用户:{} ,活动ID:{}", uId, activityId);
            return new DrawRes(ResponseCode.DUPLICATE_REQUEST);
        }
        //续期，避免开奖过程中过期
        stringRedisTemplate.opsForValue().set(userAwardKey, data, 1, TimeUnit.HOURS);
        DrawAwardVO drawAwardVO = JSONUtil.toBean(data, DrawAwardVO.class);
        try {
            //检测Redis缓存中是否有奖品信息 【缓存兜底】
            openAwardProcess.checkRedisAndSet(activityId);

            //扣活动库存【redis乐观锁】
            String activityStockKey = "activityTotalCount:" + activityId;
            boolean success = optimisticLock.deductActivityStock(activityStockKey);
            if (!success) {
                log.info("活动库存不足... 用户:{} ,活动ID:{}", uId, activityId);
                return new DrawRes(ResponseCode.LOSING_DRAW.getCode(), ResponseCode.LOSING_DRAW.getInfo());
            }

            //扣奖品库存【redis乐观锁】
            String awardStockListKey = "awardInfoList:" + activityId;
            success = optimisticLock.deductAwardStock(awardStockListKey, drawAwardVO.getAwardId());
            if (BooleanUtil.isFalse(success)) {
                //活动库存回退
                stringRedisTemplate.opsForValue().increment(activityStockKey, 1);
                log.info("奖品库存不足... 用户:{} ,活动ID:{}", uId, activityId);
                //库存不够，默认未中奖 TODO: 默认奖品实现自定义？
                return new DrawRes(ResponseCode.LOSING_DRAW.getCode(), ResponseCode.LOSING_DRAW.getInfo());
            }

            /* 此处不需要并发控制了，扣奖品的总库存 */
            String awardTotalCountKey = "awardTotalStockCount:" + activityId;
            String awardTotalCount = stringRedisTemplate.opsForValue().get(awardTotalCountKey);
            if (awardTotalCount != null && Integer.parseInt(awardTotalCount) > 0) {
                stringRedisTemplate.opsForValue().increment(awardTotalCountKey, -1);
            }
            drawAwardVO.setUId(uId);
            // 扣除个人参与次数,并写入中奖记录
            openAwardProcess.deductUserLeftCount(activityId,drawAwardVO);

            /* MQ异步处理 */
            openAwardProcess.sendMqToDisAward(drawAwardVO);

            //扣库存成功就删除Redis中奖品缓存
            stringRedisTemplate.delete(userAwardKey);
            return buildDrawRes(activityId, drawAwardVO);
        } catch (Exception e) {
            //Redis库存回退
            openAwardProcess.redisStockBack(activityId, drawAwardVO.getAwardId());
            return new DrawRes(ResponseCode.UN_ERROR.getCode(), "开奖异常："+e.getMessage());
        } finally {
            lock.unlock();
        }
    }

    @Override
    public Result createActivity(ActivityConfig createActivityReq) {

        try {
            //校验
            if (createActivityReq.getStockCount() == null || createActivityReq.getStockCount() <= 0) {
                return Result.buildErrorResult("活动库存数量不合法！");
            }
            if (createActivityReq.getTakeCount() == null || createActivityReq.getTakeCount() <= 0) {
                return Result.buildErrorResult("每人可参与次数不合法！");
            }
            Boolean exist = strategyRepository.hasThisStrategy(createActivityReq.getStrategyId());
            if (BooleanUtil.isFalse(exist)) {
                return Result.buildErrorResult("该策略不存在！");
            }
            //插入数据库
            ActivityVO activityVO = new ActivityVO();
            BeanUtil.copyProperties(createActivityReq, activityVO);
            activityVO.setState(ActivityState.EDIT.getCode());
            activityDeploy.createActivity(activityVO);

            //设置定时任务  beginTime的前一小时
            String corn = CronUtil.convertDateToCorn(beforeOneHourForDate(activityVO.getBeginDateTime()));
            JobConfig jobConfig = new JobConfig("initRedisJob-" + activityVO.getActivityId(), corn);
            jobConfig.setJobDescription(createActivityReq.getActivityDesc());
            initActivityJob.setActivityVO(activityVO);
            initJobSchedulerHandler.init(initActivityJob, jobConfig);
        } catch (Exception e) {
            return Result.buildErrorResult("创建活动失败！msg:" + e.getMessage());
        }

        return Result.buildSuccessResult();
    }

    public static Date beforeOneHourForDate(Date date) {
        Calendar calendar = Calendar.getInstance();
        /* HOUR_OF_DAY 指示一天中的小时 */
        calendar.setTime(date);
        calendar.add(Calendar.HOUR_OF_DAY, -1);
        return calendar.getTime();
    }

    private DrawRes buildDrawRes(Long activityId, DrawAwardVO drawAwardVO) {
        DrawRes drawRes = new DrawRes(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getInfo());
        AwardDTO awardDTO = awardMapping.sourceToTarget(drawAwardVO);
        awardDTO.setActivityId(activityId);
        drawRes.setAwardDTO(awardDTO);
        return drawRes;
    }
}
